AJL protocが… 見つからない!?

概要

あなたは現在、プログラミングサークルのPaaS基盤の開発に勤しんでいます。このPaaSでは、Dockerfileを用いてイメージをビルドし、そのイメージを動かすことができます。

さて、ここで共同開発している先輩がサンプルアプリケーションを作ってくれました。GoとgRPCを使ったシンプルなPingサーバーで、先輩からは「Protocol Buffersのビルド環境の公式イメージが無いから、ちゃちゃっと作ってくれん?ついでにGoも入ってると嬉しいわ」と言われてしまいました。

builderイメージの作成はスムーズに行っているように思われましたが、試しに先輩の作ってくれたDockerfileをビルドしてみたところ…

「/bin/sh: protoc: not found」

protocが… 見つからない!?

前提条件

  • 禁止されるアクション
    • 先輩が用意した (/home/user/app ディレクトリ内にある) ファイルの変更
    • デプロイスクリプト (/home/user/deploy ファイル) の変更
    • Protocol Buffersならびに使用するベースイメージの、公式ソース以外からの取得
    • builderイメージへの、ローカルからのファイルのコピー (--from タグを指定しないCOPYコマンドや、Volumeによるマウントの使用)
    • 最新版 (v22.0 / v3.22.0) 以外のProtocol Buffersの使用
    • ベースイメージからのあらゆるファイルの削除
  • その他
    • 共同開発している先輩には知見を共有する必要があるので、先輩が納得するように今回のトラブルの原因を説明してください
    • 先輩はある程度Docker、Linux、Protocol Buffersに関する知識があるという前提でOKです

初期状態

/home/user/deploy を実行すると、ビルド中に /bin/sh: protoc: not found エラーが出て終了する

終了状態

/home/user/deploy を実行した時、

  • Protocol BuffersとGoのビルドができる汎用イメージである、builder:v1 イメージがビルドされ、docker image ls で閲覧できるリスト上に出る
  • builder:v1 イメージのサイズが300MB以下である
  • 上記イメージを使用した先輩のアプリケーションのDockerビルドとコンテナの起動が正常に完了する
  • localhost:8080 上に先輩の作ったgRPCサーバーが立つ
    • web-serverホスト上にて grpcurl -plaintext :8080 app.protobuf.PingService/Ping を実行し、以下のレスポンスが返ってくるなどで確認できる
    • このコマンド実行のためには別途grpcurlのダウンロードが必要
{
  "message": "Hello, ICTSC2022 Contestant!"
}

問題環境の再現方法

  1. CPU5コア、メモリ4GBのUbuntuが動いているVMを用意します。
    • 想定解のビルドが大変重いので本戦はこの構成になっていましたが、マシン性能に関しては自由で構いません。
  2. userユーザーを作成します。
  3. Dockerをインストールします。
  4. userユーザーに切り替えます。
  5. https://github.com/logica0419/ictsc2022/tree/main/protobuf-alpine にあるファイル・フォルダ類を、/home/user に追加します。
    • /home/user 直下に app ディレクトリ、builder ディレクトリ、deploy ファイルがある形になります。
  6. chmod +x /home/user/deploy をターミナルで実行し、deployファイルを実行可能にします。
  7. /home/user/deploy をターミナルで実行すると、初期状態が再現します。

解説

protocはPATH上にあるため、最初のbuilderイメージに適当なEntrypointを設定して起動し which protoc を実行するとパスが帰ってくるはずである。これでも実行時に見つからないと言われるのは、以下が原因。
https://stackoverflow.com/questions/64447731/protoc-not-found-on-an-alpine-based-docker-container-running-protocol-buffers

多くのLinuxディストリビューションはGNU C Libraryを標準Cライブラリとして持っているが、Alpineの標準Cライブラリはmuslと呼ばれる全く別規格のCライブラリである。protocの配布バイナリはGNUライブラリのいくつかにDynamic Linkで依存してビルドされているため、これを持たないAlpineはnot foundというエラーを吐く。

amdとarmのCPUアーキテクチャ違いのバイナリを実行しようとした時でもこのような現象が起こる。

1番簡単に解決する手段はGNUライブラリが標準になっているディストリビューションへの移行だが、これをするだけだとアプリケーションの実行時点で同様のnot foundエラーが出る。GoのアプリケーションをCGOが無効の状態でビルドするようにすれば解決はするが、Goの公式イメージ(Debian)では300MB制限を突破することができない。

以上のことより、今回の問題の想定解はAlpine上で動くprotocバイナリを用意することである。だが、Protocol Buffers v22.0は2月の終わりに出たメジャーアップデートで、ICTSC2022本戦時点でapkやbrew上からは入手不可能だった。公式が用意したソース以外からの入手は禁じたため、builderイメージ上でprotocをソースコードから直接ビルドすることが必要になる。

また、Protocol BuffersでGoのコードを自動生成するためには、専用プラグインであるprotoc-gen-goとprotoc-gen-go-grpcを入れなければいけない。
https://grpc.io/docs/languages/go/quickstart/

以上を踏まえた想定解のDockerfileがこちら

FROM golang:1.20.1-alpine AS dl

WORKDIR /download
RUN echo "@testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
  apk add --no-cache unzip g++ linux-headers bazel4@testing && \
  wget -q "https://github.com/protocolbuffers/protobuf/archive/refs/tags/v22.0.zip" -O "protobuf.zip" && \
  unzip -o protobuf.zip -d protobuf && chmod -R 755 protobuf/* && \
  go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \
  go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

WORKDIR /download/protobuf/protobuf-22.0
RUN bazel build :protoc :protobuf

FROM golang:1.20.1-alpine AS builder

RUN apk add --no-cache libstdc++

COPY --from=dl /download/protobuf/protobuf-22.0/bazel-bin/protoc /usr/local/bin/
COPY --from=dl /go/bin/protoc-gen-go /usr/local/bin/
COPY --from=dl /go/bin/protoc-gen-go-grpc /usr/local/bin/

これでビルドすると、builder:v1 イメージのサイズが291MBとなり、終了条件を満たす。

また、bazelを使わない方法でビルドしていた解答もあったが、ビルド方法にかかわらず終了条件を満たせば正解とした。

別解

apkからprotocに必要な共有ライブラリとGNU互換性ライブラリをインストールすることでも解決できる。

lddを使って調査するとlibstdc++およびlibgcが足りていないことがわかるため、libstdc++を追加するのとGNUライブラリの互換性ライブラリであるgcompatを追加することで正常に動かすことができる。gcompatはmuslライブラリをGNUライクなAPIで叩けるようにしてくれるライブラリで、これを使うことでGNUライブラリでビルドされたバイナリをAlpine上で実行できる。
https://wiki.alpinelinux.org/wiki/Running_glibc_programs

FROM golang:1.20.1-alpine AS dl

WORKDIR /download

RUN apk add --no-cache unzip && \
  wget -q "https://github.com/protocolbuffers/protobuf/releases/download/v22.0/protoc-22.0-linux-x86_64.zip" -O "protobuf.zip" && \
  unzip -o protobuf.zip -d protobuf && chmod -R 755 protobuf/* && \
  go install google.golang.org/protobuf/cmd/[email protected] && \
  go install google.golang.org/grpc/cmd/[email protected]

FROM golang:1.20.1-alpine AS builder

RUN apk add --no-cache libstdc++ gcompat

COPY --from=dl /download/protobuf/bin/protoc /usr/local/bin/
COPY --from=dl /go/bin/protoc-gen-go /usr/local/bin/
COPY --from=dl /go/bin/protoc-gen-go-grpc /usr/local/bin/

この回答ではbuilder:v1 イメージのサイズが280MBとなり、終了条件を満たす。

採点基準

「突き止められている」はそのことに解答で言及しているかどうかで判定をした。

  • 前提条件を無視している場合
    • 問答無用で0点
  • protocがGNUライブラリに依存し、Alpineはmusl or 共有ライブラリが足りていない という原因を突き止められている
    • 75 点
  • protocのDockerfile上でのビルド or 共有ライブラリのインストール がされており、not foundエラーが解消されている
    • 150 点
  • Goのプラグインが無いという原因を突き止められている
    • 25 点
  • Goのプラグインを用意できている
    • 50 点
  • 終了条件の中でもサイズが300MBに収まっているという条件をクリアできていない時、以下のように採点した
    • 一部しか問題が解決できていない場合
      • その後の操作で自然と条件がクリアできるとは考えられないため0点
    • サイズ条件以外の全てをクリアできている場合
      • 半分の150点

作問者コメント

  • 競技開始1時間ほどで、運営の中で出た解答であるディストリビューションの載せ替えを縛るためにbuilderイメージのサイズ制限を追加 (問題文中太字の部分) したのだが、追加作業中に来た解答がサイズ制限にのみ引っかかる解答で大変申し訳なかった。この場でも深くお詫び申し上げます。
  • ほとんどの正答がGNU互換ライブラリを使用するもの (別解) であった。protocの自分でのビルドを想定解としていたので、少々さみしい気持ちがある。
  • 1日目は競技開始から3時間半ほどで上位陣から5件解答があったのち開始5時間半時点で1件解答があったのみ、2日目はパラパラと解答がある、という形の時間分布をしていたので
    • C / Linuxに詳しい人・似たトラブルにあった人は当たりがつく
    • あまり詳しくない人は原因が謎のまま沼る
    という問題だったのかなと思う